import _ from 'lodash';
import preconditions from 'preconditions';
import { Common } from './common';
import { Errors } from './errors/errordefinitions';
import { Storage } from './storage';

const $ = preconditions.singleton();

const Defaults = Common.Defaults;
const ACQUIRE_RETRY_STEP = 50; // ms

export class Lock {
  storage: Storage;
  constructor(storage: Storage, _opts = {}) {
    _opts = _opts || {};

    this.storage = storage;
  }

  acquire(token, opts, cb, timeLeft?) {
    opts = opts || {};

    opts.lockTime = opts.lockTime || Defaults.LOCK_EXE_TIME;

    this.storage.acquireLock(token, Date.now() + opts.lockTime, err => {
      // Lock taken?
      if (err && err.message && err.message.indexOf('E11000 ') !== -1) {
        // Lock expired?
        this.storage.clearExpiredLock(token, () => {});
        // Waiting time for lock has expired
        if (timeLeft < 0) {
          return cb('LOCKED');
        }

        if (!_.isUndefined(opts.waitTime)) {
          if (_.isUndefined(timeLeft)) {
            timeLeft = opts.waitTime;
          } else {
            timeLeft -= ACQUIRE_RETRY_STEP;
          }
        }

        return setTimeout(this.acquire.bind(this, token, opts, cb, timeLeft), ACQUIRE_RETRY_STEP);

        // Actual DB error
      } else if (err) {
        return cb(err);

        // Lock available
      } else {
        return cb(null, icb => {
          if (!icb) icb = () => {};
          this.storage.releaseLock(token, icb);
        });
      }
    });
  }

  runLocked(token, opts, cb, task) {
    $.shouldBeDefined(token, 'Failed state: token undefined at <runLocked()>');

    this.acquire(token, opts, (err, release) => {
      if (err == 'LOCKED') return cb(Errors.WALLET_BUSY);
      if (err) return cb(err);

      const _cb = function(...args) {
        cb(...args);
        release();
      };
      task(_cb);
    });
  }
}
